Passed
Branch v8.x (a9c55b)
by Rafael S.
04:46 queued 01:17
created

index.js ➔ toMuLaw   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 10
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 11
rs 9.9
1
/*
2
 * Copyright (c) 2017-2018 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from './vendor/bitdepth.js';
33
import * as imaadpcm from './vendor/imaadpcm.js';
34
import * as alawmulaw from './vendor/alawmulaw.js';
35
import {encode, decode} from './vendor/base64-arraybuffer-es6.js';
36
import {unpackArray, packArrayTo, unpackArrayTo} from './vendor/byte-data.js';
37
import {wavHeader, AUDIO_FORMATS} from './lib/wavheader.js';
38
// @type {WavIO}
39
import WavIO from './lib/wavio.js';
40
41
/**
42
 * Class representing a wav file.
43
 * @extends WavIO
44
 */
45
export default class WaveFile extends WavIO {
46
47
  /**
48
   * @param {?Uint8Array} bytes A wave file buffer.
49
   * @throws {Error} If no 'RIFF' chunk is found.
50
   * @throws {Error} If no 'fmt ' chunk is found.
51
   * @throws {Error} If no 'data' chunk is found.
52
   */
53
  constructor(bytes=null) {
54
    super();
55
    // Load a file from the buffer if one was passed
56
    // when creating the object
57
    if(bytes) {
58
      this.fromBuffer(bytes);
59
    }
60
  }
61
62
  /**
63
   * Set up the WaveFile object based on the arguments passed.
64
   * @param {number} numChannels The number of channels
65
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
66
   * @param {number} sampleRate The sample rate.
67
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
68
   * @param {string} bitDepthCode The audio bit depth code.
69
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
70
   *    or any value between '8' and '32' (like '12').
71
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples
72
   *    The samples. Must be in the correct range according to the bit depth.
73
   * @param {?Object} options Optional. Used to force the container
74
   *    as RIFX with {'container': 'RIFX'}
75
   * @throws {Error} If any argument does not meet the criteria.
76
   */
77
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
78
    if (!options.container) {
79
      options.container = 'RIFF';
80
    }
81
    this.container = options.container;
82
    this.bitDepth = bitDepthCode;
83
    samples = this.interleave_(samples);
84
    /** @type {number} */
85
    let numBytes = (((parseInt(bitDepthCode, 10) - 1) | 7) + 1) / 8;
86
    this.updateDataType_();
87
    this.data.samples = new Uint8Array(samples.length * numBytes);
88
    packArrayTo(samples, this.dataType, this.data.samples);
89
    /** @type {!Object} */
90
    let header = wavHeader(
91
      bitDepthCode, numChannels, sampleRate,
92
      numBytes, this.data.samples.length, options);
93
    this.clearHeader_();
94
    this.chunkSize = header.chunkSize;
95
    this.format = header.format;
96
    this.fmt = header.fmt;
97
    if (header.fact) {
98
      this.fact = header.fact;
99
    }
100
    this.data.chunkId = 'data';
101
    this.data.chunkSize = this.data.samples.length;
102
    this.validateHeader_();
103
    this.LEorBE_();
104
  }
105
106
  /**
107
   * Set up the WaveFile object from a byte buffer.
108
   * @param {!Uint8Array} bytes The buffer.
109
   * @param {boolean=} samples True if the samples should be loaded.
110
   * @throws {Error} If container is not RIFF, RIFX or RF64.
111
   * @throws {Error} If no 'fmt ' chunk is found.
112
   * @throws {Error} If no 'data' chunk is found.
113
   */
114
  fromBuffer(bytes, samples=true) {
115
    this.readWavBuffer(bytes, samples);
116
  }
117
118
  /**
119
   * Return a byte buffer representig the WaveFile object as a .wav file.
120
   * The return value of this method can be written straight to disk.
121
   * @return {!Uint8Array} A .wav file.
122
   * @throws {Error} If any property of the object appears invalid.
123
   */
124
  toBuffer() {
125
    this.validateHeader_();
126
    return this.createWaveFile_();
127
  }
128
129
  /**
130
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
131
   * @param {string} base64String A .wav file as a base64 string.
132
   * @throws {Error} If any property of the object appears invalid.
133
   */
134
  fromBase64(base64String) {
135
    this.fromBuffer(new Uint8Array(decode(base64String)));
136
  }
137
138
  /**
139
   * Return a base64 string representig the WaveFile object as a .wav file.
140
   * @return {string} A .wav file as a base64 string.
141
   * @throws {Error} If any property of the object appears invalid.
142
   */
143
  toBase64() {
144
    /** @type {!Uint8Array} */
145
    let buffer = this.toBuffer();
146
    return encode(buffer, 0, buffer.length);
147
  }
148
149
  /**
150
   * Return a DataURI string representig the WaveFile object as a .wav file.
151
   * The return of this method can be used to load the audio in browsers.
152
   * @return {string} A .wav file as a DataURI.
153
   * @throws {Error} If any property of the object appears invalid.
154
   */
155
  toDataURI() {
156
    return 'data:audio/wav;base64,' + this.toBase64();
157
  }
158
159
  /**
160
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
161
   * @param {string} dataURI A .wav file as DataURI.
162
   * @throws {Error} If any property of the object appears invalid.
163
   */
164
  fromDataURI(dataURI) {
165
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
166
  }
167
168
  /**
169
   * Force a file as RIFF.
170
   */
171
  toRIFF() {
172
    if (this.container == 'RF64') {
173
      this.fromScratch(
174
        this.fmt.numChannels,
175
        this.fmt.sampleRate,
176
        this.bitDepth,
177
        unpackArray(this.data.samples, this.dataType));
178
    } else {
179
      this.dataType.be = true;
180
      this.fromScratch(
181
        this.fmt.numChannels,
182
        this.fmt.sampleRate,
183
        this.bitDepth,
184
        unpackArray(this.data.samples, this.dataType));
185
    }
186
  }
187
188
  /**
189
   * Force a file as RIFX.
190
   */
191
  toRIFX() {
192
    if (this.container == 'RF64') {
193
      this.fromScratch(
194
        this.fmt.numChannels,
195
        this.fmt.sampleRate,
196
        this.bitDepth,
197
        unpackArray(this.data.samples, this.dataType),
198
        {container: 'RIFX'});
199
    } else {
200
      this.fromScratch(
201
        this.fmt.numChannels,
202
        this.fmt.sampleRate,
203
        this.bitDepth,
204
        unpackArray(this.data.samples, this.dataType),
205
        {container: 'RIFX'});
206
    }
207
  }
208
209
  /**
210
   * Change the bit depth of the samples.
211
   * @param {string} newBitDepth The new bit depth of the samples.
212
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
213
   * @param {boolean} changeResolution A boolean indicating if the
214
   *    resolution of samples should be actually changed or not.
215
   * @throws {Error} If the bit depth is not valid.
216
   */
217
  toBitDepth(newBitDepth, changeResolution=true) {
218
    // @type {string}
219
    let toBitDepth = newBitDepth;
220
    // @type {string}
221
    let thisBitDepth = this.bitDepth;
222
    if (!changeResolution) {
223
      toBitDepth = this.realBitDepth_(newBitDepth);
224
      thisBitDepth = this.realBitDepth_(this.bitDepth);
225
    }
226
    this.assureUncompressed_();
227
    // @type {number}
228
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
229
    // @type {!Float64Array}
230
    let typedSamplesInput = new Float64Array(sampleCount + 1);
231
    // @type {!Float64Array}
232
    let typedSamplesOutput = new Float64Array(sampleCount + 1);
233
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
234
    bitDepthLib(
235
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
236
    this.fromScratch(
237
      this.fmt.numChannels,
238
      this.fmt.sampleRate,
239
      newBitDepth,
240
      typedSamplesOutput,
241
      {container: this.correctContainer_()});
242
  }
243
244
  /**
245
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
246
   * @throws {Error} If sample rate is not 8000.
247
   * @throws {Error} If number of channels is not 1.
248
   */
249
  toIMAADPCM() {
250
    if (this.fmt.sampleRate !== 8000) {
251
      throw new Error(
252
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
253
    } else if(this.fmt.numChannels !== 1) {
254
      throw new Error(
255
        'Only mono files can be compressed as IMA-ADPCM.');
256
    } else {
257
      this.assure16Bit_();
258
      let output = new Int16Array(this.data.samples.length / 2);
259
      unpackArrayTo(this.data.samples, this.dataType, output);
260
      this.fromScratch(
261
        this.fmt.numChannels,
262
        this.fmt.sampleRate,
263
        '4',
264
        imaadpcm.encode(output),
265
        {container: this.correctContainer_()});
266
    }
267
  }
268
269
  /**
270
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
271
   * @param {string} bitDepthCode The new bit depth of the samples.
272
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
273
   *    Optional. Default is 16.
274
   */
275
  fromIMAADPCM(bitDepthCode='16') {
276
    this.fromScratch(
277
      this.fmt.numChannels,
278
      this.fmt.sampleRate,
279
      '16',
280
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
281
      {container: this.correctContainer_()});
282
    if (bitDepthCode != '16') {
283
      this.toBitDepth(bitDepthCode);
284
    }
285
  }
286
287
  /**
288
   * Encode a 16-bit wave file as 8-bit A-Law.
289
   */
290
  toALaw() {
291
    this.assure16Bit_();
292
    let output = new Int16Array(this.data.samples.length / 2);
293
    unpackArrayTo(this.data.samples, this.dataType, output);
294
    this.fromScratch(
295
      this.fmt.numChannels,
296
      this.fmt.sampleRate,
297
      '8a',
298
      alawmulaw.alaw.encode(output),
299
      {container: this.correctContainer_()});
300
  }
301
302
  /**
303
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
304
   * @param {string} bitDepthCode The new bit depth of the samples.
305
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
306
   *    Optional. Default is 16.
307
   */
308
  fromALaw(bitDepthCode='16') {
309
    this.fromScratch(
310
      this.fmt.numChannels,
311
      this.fmt.sampleRate,
312
      '16',
313
      alawmulaw.alaw.decode(this.data.samples),
314
      {container: this.correctContainer_()});
315
    if (bitDepthCode != '16') {
316
      this.toBitDepth(bitDepthCode);
317
    }
318
  }
319
320
  /**
321
   * Encode 16-bit wave file as 8-bit mu-Law.
322
   */
323
  toMuLaw() {
324
    this.assure16Bit_();
325
    let output = new Int16Array(this.data.samples.length / 2);
326
    unpackArrayTo(this.data.samples, this.dataType, output);
327
    this.fromScratch(
328
      this.fmt.numChannels,
329
      this.fmt.sampleRate,
330
      '8m',
331
      alawmulaw.mulaw.encode(output),
332
      {container: this.correctContainer_()});
333
  }
334
335
  /**
336
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
337
   * @param {string} bitDepthCode The new bit depth of the samples.
338
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
339
   *    Optional. Default is 16.
340
   */
341
  fromMuLaw(bitDepthCode='16') {
342
    this.fromScratch(
343
      this.fmt.numChannels,
344
      this.fmt.sampleRate,
345
      '16',
346
      alawmulaw.mulaw.decode(this.data.samples),
347
      {container: this.correctContainer_()});
348
    if (bitDepthCode != '16') {
349
      this.toBitDepth(bitDepthCode);
350
    }
351
  }
352
353
  /**
354
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
355
   * then it is created. It if exists, it is overwritten.
356
   * @param {string} tag The tag name.
357
   * @param {string} value The tag value.
358
   * @throws {Error} If the tag name is not valid.
359
   */
360
  setTag(tag, value) {
361
    tag = this.fixTagName_(tag);
362
    /** @type {!Object} */
363
    let index = this.getTagIndex_(tag);
364
    if (index.TAG !== null) {
365
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
366
        value.length + 1;
367
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
368
    } else if (index.LIST !== null) {
369
      this.LIST[index.LIST].subChunks.push({
370
        chunkId: tag,
371
        chunkSize: value.length + 1,
372
        value: value});
373
    } else {
374
      this.LIST.push({
375
        chunkId: 'LIST',
376
        chunkSize: 8 + value.length + 1,
377
        format: 'INFO',
378
        subChunks: []});
379
      this.LIST[this.LIST.length - 1].subChunks.push({
380
        chunkId: tag,
381
        chunkSize: value.length + 1,
382
        value: value});
383
    }
384
  }
385
386
  /**
387
   * Return the value of a RIFF tag in the INFO chunk.
388
   * @param {string} tag The tag name.
389
   * @return {?string} The value if the tag is found, null otherwise.
390
   */
391
  getTag(tag) {
392
    /** @type {!Object} */
393
    let index = this.getTagIndex_(tag);
394
    if (index.TAG !== null) {
395
      return this.LIST[index.LIST].subChunks[index.TAG].value;
396
    }
397
    return null;
398
  }
399
400
  /**
401
   * Remove a RIFF tag in the INFO chunk.
402
   * @param {string} tag The tag name.
403
   * @return {boolean} True if a tag was deleted.
404
   */
405
  deleteTag(tag) {
406
    /** @type {!Object} */
407
    let index = this.getTagIndex_(tag);
408
    if (index.TAG !== null) {
409
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
410
      return true;
411
    }
412
    return false;
413
  }
414
415
  /**
416
   * Create a cue point in the wave file.
417
   * @param {number} position The cue point position in milliseconds.
418
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
419
   */
420
  setCuePoint(position, labl='') {
421
    this.cue.chunkId = 'cue ';
422
    position = (position * this.fmt.sampleRate) / 1000;
423
    /** @type {!Array<!Object>} */
424
    let existingPoints = this.getCuePoints_();
425
    this.clearLISTadtl_();
426
    /** @type {number} */
427
    let len = this.cue.points.length;
428
    this.cue.points = [];
429
    /** @type {boolean} */
430
    let hasSet = false;
431
    if (len === 0) {
432
      this.setCuePoint_(position, 1, labl);
433
    } else {
434
      for (let i=0; i<len; i++) {
435
        if (existingPoints[i].dwPosition > position && !hasSet) {
436
          this.setCuePoint_(position, i + 1, labl);
437
          this.setCuePoint_(
438
            existingPoints[i].dwPosition,
439
            i + 2,
440
            existingPoints[i].label);
441
          hasSet = true;
442
        } else {
443
          this.setCuePoint_(
444
            existingPoints[i].dwPosition,
445
            i + 1,
446
            existingPoints[i].label);
447
        }
448
      }
449
      if (!hasSet) {
450
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
451
      }
452
    }
453
    this.cue.dwCuePoints = this.cue.points.length;
454
  }
455
456
  /**
457
   * Remove a cue point from a wave file.
458
   * @param {number} index the index of the point. First is 1,
459
   *    second is 2, and so on.
460
   */
461
  deleteCuePoint(index) {
462
    this.cue.chunkId = 'cue ';
463
    /** @type {!Array<!Object>} */
464
    let existingPoints = this.getCuePoints_();
465
    this.clearLISTadtl_();
466
    /** @type {number} */
467
    let len = this.cue.points.length;
468
    this.cue.points = [];
469
    for (let i=0; i<len; i++) {
470
      if (i + 1 !== index) {
471
        this.setCuePoint_(
472
          existingPoints[i].dwPosition,
473
          i + 1,
474
          existingPoints[i].label);
475
      }
476
    }
477
    this.cue.dwCuePoints = this.cue.points.length;
478
    if (this.cue.dwCuePoints) {
479
      this.cue.chunkId = 'cue ';
480
    } else {
481
      this.cue.chunkId = '';
482
      this.clearLISTadtl_();
483
    }
484
  }
485
486
  /**
487
   * Update the label of a cue point.
488
   * @param {number} pointIndex The ID of the cue point.
489
   * @param {string} label The new text for the label.
490
   */
491
  updateLabel(pointIndex, label) {
492
    /** @type {?number} */
493
    let adtlIndex = this.getAdtlChunk_();
494
    if (adtlIndex !== null) {
495
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
496
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
497
            pointIndex) {
498
          this.LIST[adtlIndex].subChunks[i].value = label;
499
        }
500
      }
501
    }
502
  }
503
504
  /**
505
   * Make the file 16-bit if it is not.
506
   * @private
507
   */
508
  assure16Bit_() {
509
    this.assureUncompressed_();
510
    if (this.bitDepth != '16') {
511
      this.toBitDepth('16');
512
    }
513
  }
514
515
  /**
516
   * Uncompress the samples in case of a compressed file.
517
   * @private
518
   */
519
  assureUncompressed_() {
520
    if (this.bitDepth == '8a') {
521
      this.fromALaw();
522
    } else if(this.bitDepth == '8m') {
523
      this.fromMuLaw();
524
    } else if (this.bitDepth == '4') {
525
      this.fromIMAADPCM();
526
    }
527
  }
528
  
529
  /**
530
   * Set up the WaveFile object from a byte buffer.
531
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples The samples.
532
   * @private
533
   */
534
  interleave_(samples) {
535
    if (samples.length > 0) {
536
      if (samples[0].constructor === Array) {
537
        /** @type {!Array<number>} */
538
        let finalSamples = [];
539
        for (let i=0; i < samples[0].length; i++) {
540
          for (let j=0; j < samples.length; j++) {
541
            finalSamples.push(samples[j][i]);
542
          }
543
        }
544
        samples = finalSamples;
545
      }
546
    }
547
    return samples;
548
  }
549
550
  /**
551
   * Push a new cue point in this.cue.points.
552
   * @param {number} position The position in milliseconds.
553
   * @param {number} dwName the dwName of the cue point
554
   * @private
555
   */
556
  setCuePoint_(position, dwName, label) {
557
    this.cue.points.push({
558
      dwName: dwName,
559
      dwPosition: position,
560
      fccChunk: 'data',
561
      dwChunkStart: 0,
562
      dwBlockStart: 0,
563
      dwSampleOffset: position,
564
    });
565
    this.setLabl_(dwName, label);
566
  }
567
568
  /**
569
   * Return an array with the position of all cue points in the file.
570
   * @return {!Array<!Object>}
571
   * @private
572
   */
573
  getCuePoints_() {
574
    /** @type {!Array<!Object>} */
575
    let points = [];
576
    for (let i=0; i<this.cue.points.length; i++) {
577
      points.push({
578
        dwPosition: this.cue.points[i].dwPosition,
579
        label: this.getLabelForCuePoint_(
580
          this.cue.points[i].dwName)});
581
    }
582
    return points;
583
  }
584
585
  /**
586
   * Return the label of a cue point.
587
   * @param {number} pointDwName The ID of the cue point.
588
   * @return {string}
589
   * @private
590
   */
591
  getLabelForCuePoint_(pointDwName) {
592
    /** @type {?number} */
593
    let adtlIndex = this.getAdtlChunk_();
594
    if (adtlIndex !== null) {
595
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
596
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
597
            pointDwName) {
598
          return this.LIST[adtlIndex].subChunks[i].value;
599
        }
600
      }
601
    }
602
    return '';
603
  }
604
605
  /**
606
   * Clear any LIST chunk labeled as 'adtl'.
607
   * @private
608
   */
609
  clearLISTadtl_() {
610
    for (let i=0; i<this.LIST.length; i++) {
611
      if (this.LIST[i].format == 'adtl') {
612
        this.LIST.splice(i);
613
      }
614
    }
615
  }
616
617
  /**
618
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
619
   * @param {number} dwName The ID of the cue point.
620
   * @param {string} label The label for the cue point.
621
   * @private
622
   */
623
  setLabl_(dwName, label) {
624
    /** @type {?number} */
625
    let adtlIndex = this.getAdtlChunk_();
626
    if (adtlIndex === null) {
627
      this.LIST.push({
628
        chunkId: 'LIST',
629
        chunkSize: 4,
630
        format: 'adtl',
631
        subChunks: []});
632
      adtlIndex = this.LIST.length - 1;
633
    }
634
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
635
  }
636
637
  /**
638
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
639
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
640
   * @param {number} dwName The ID of the cue point.
641
   * @param {string} label The label for the cue point.
642
   * @private
643
   */
644
  setLabelText_(adtlIndex, dwName, label) {
645
    this.LIST[adtlIndex].subChunks.push({
646
      chunkId: 'labl',
647
      chunkSize: label.length,
648
      dwName: dwName,
649
      value: label
650
    });
651
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
652
  }
653
654
  /**
655
   * Return the index of the 'adtl' LIST in this.LIST.
656
   * @return {?number}
657
   * @private
658
   */
659
  getAdtlChunk_() {
660
    for (let i=0; i<this.LIST.length; i++) {
661
      if(this.LIST[i].format == 'adtl') {
662
        return i;
663
      }
664
    }
665
    return null;
666
  }
667
668
  /**
669
   * Return the index of a tag in a FILE chunk.
670
   * @param {string} tag The tag name.
671
   * @return {!Object<string, ?number>}
672
   *    Object.LIST is the INFO index in LIST
673
   *    Object.TAG is the tag index in the INFO
674
   * @private
675
   */
676
  getTagIndex_(tag) {
677
    /** @type {!Object<string, ?number>} */
678
    let index = {LIST: null, TAG: null};
679
    for (let i=0; i<this.LIST.length; i++) {
680
      if (this.LIST[i].format == 'INFO') {
681
        index.LIST = i;
682
        for (let j=0; j<this.LIST[i].subChunks.length; j++) {
683
          if (this.LIST[i].subChunks[j].chunkId == tag) {
684
            index.TAG = j;
685
            break;
686
          }
687
        }
688
        break;
689
      }
690
    }
691
    return index;
692
  }
693
694
  /**
695
   * Fix a RIFF tag format if possible, throw an error otherwise.
696
   * @param {string} tag The tag name.
697
   * @return {string} The tag name in proper fourCC format.
698
   * @private
699
   */
700
  fixTagName_(tag) {
701
    if (tag.constructor !== String) {
702
      throw new Error('Invalid tag name.');
703
    } else if(tag.length < 4) {
704
      for (let i=0; i<4-tag.length; i++) {
705
        tag += ' ';
706
      }
707
    }
708
    return tag;
709
  }
710
711
  /**
712
   * Validate the header of the file.
713
   * @throws {Error} If any property of the object appears invalid.
714
   * @private
715
   */
716
  validateHeader_() {
717
    this.validateBitDepth_();
718
    this.validateNumChannels_();
719
    this.validateSampleRate_();
720
  }
721
722
  /**
723
   * Validate the bit depth.
724
   * @return {boolean} True is the bit depth is valid.
725
   * @throws {Error} If bit depth is invalid.
726
   * @private
727
   */
728
  validateBitDepth_() {
729
    if (!AUDIO_FORMATS[this.bitDepth]) {
730
      if (parseInt(this.bitDepth, 10) > 8 &&
731
          parseInt(this.bitDepth, 10) < 54) {
732
        return true;
733
      }
734
      throw new Error('Invalid bit depth.');
735
    }
736
    return true;
737
  }
738
739
  /**
740
   * Validate the number of channels.
741
   * @return {boolean} True is the number of channels is valid.
742
   * @throws {Error} If the number of channels is invalid.
743
   * @private
744
   */
745
  validateNumChannels_() {
746
    /** @type {number} */
747
    let blockAlign = this.fmt.numChannels * this.fmt.bitsPerSample / 8;
748
    if (this.fmt.numChannels < 1 || blockAlign > 65535) {
749
      throw new Error('Invalid number of channels.');
750
    }
751
    return true;
752
  }
753
754
  /**
755
   * Validate the sample rate value.
756
   * @return {boolean} True is the sample rate is valid.
757
   * @throws {Error} If the sample rate is invalid.
758
   * @private
759
   */
760
  validateSampleRate_() {
761
    /** @type {number} */
762
    let byteRate = this.fmt.numChannels *
763
      (this.fmt.bitsPerSample / 8) * this.fmt.sampleRate;
764
    if (this.fmt.sampleRate < 1 || byteRate > 4294967295) {
765
      throw new Error('Invalid sample rate.');
766
    }
767
    return true;
768
  }
769
770
  /**
771
   * Return the closest greater number of bits for a number of bits that
772
   * do not fill a full sequence of bytes.
773
   * @param {string} bitDepthCode The bit depth.
774
   * @return {string}
775
   * @private
776
   */
777
  realBitDepth_(bitDepthCode) {
778
    if (bitDepthCode != '32f') {
779
      bitDepthCode = (((parseInt(bitDepthCode, 10) - 1) | 7) + 1).toString();
780
    }
781
    return bitDepthCode;
782
  }
783
784
  /**
785
   * Return 'RIFF' if the container is 'RF64', the current container name
786
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
787
   * @return {string}
788
   * @private
789
   */
790
  correctContainer_() {
791
    return this.container == 'RF64' ? 'RIFF' : this.container;
792
  }
793
}
794